from __future__ import with_statement
import sys
import logging
import os
from os.path import join
import zipfile
import errno
import urllib2
import subprocess
import time
import xml.dom.minidom
import shutil
from optparse import OptionParser

from browsertests.utils import cli_wrapper
from browsertests.tests.models import Status
from browsertests.useragents.models import Useragent, Branch
try:
    import chromium_utils
except:
    sys.path.append(join(os.path.dirname(__file__), "..", "third_party"))
from chromium_utils.chromium_utils import ExtractZip, RemoveDirectory, MaybeMakeDirectory

log = logging.getLogger(__name__)

class BrowserUpdater(object):
    KEEP_OLD_BROWSERS = True

    def __init__(self, base_dir, branch_id, platform, url):
        self.base_dir = base_dir
        self.branch_id = branch_id
        self.platform = platform
        self.url = url
        self.dummy_db = False

    def _is_fresh(self):
        return False

    def _get_version(self):
        raise NotImplementedError()

    def _download(self, url, destination_dir):
        dest = join(destination_dir, url.split("/")[-1])
        log.info("Downloading '%s' to '%s'", url, dest)
        f = urllib2.urlopen(url)
        with open(dest, "wb") as output:
            output.write(f.read())
        f.close()
        log.info("download done.")
        return dest

    def _extract_archive(self, archive_path, destination_dir):
        log.info("Extracting %s to %s", archive_path, destination_dir)
        if archive_path.endswith(".exe"):
            raise NotImplementedError()
        if archive_path.endswith(".zip"):
            ExtractZip(archive_path, destination_dir, verbose=False)
        elif archive_path.endswith(".tar.bz2"):
            subprocess.check_call(["tar", "jxf", archive_path],
                                  cwd=destination_dir)
        elif archive_path.endswith(".tar.gz"):
            subprocess.check_call(["tar", "zxf", archive_path],
                                  cwd=destination_dir)
        elif archive_path.endswith(".dmg"):
            # See http://mxr.mozilla.org/mozilla/source/testing/release/minotaur/mozInstall.py#216
            raise NotImplementedError()
        else:
            raise Exception("Unsupported archive type (%s)" % dest)
        log.info("extracting done.")

    def _download_and_install(self, url, browser_dir, version):
        dest = self._download(url, browser_dir)
        self._extract_archive(dest, browser_dir)

    def _get_path(self, browser_dir, version):
        raise NotImplementedError()

    def _uninstall_ua(self, browser_dir, old_ua):
        if not self.KEEP_OLD_BROWSERS and old_ua.path:
            # TODO: handle .exe uninstall.

            # Remove everything one level above the browser directory.
            # Assume that the browser was installed in a hierarchy:
            # browser_dir / VERSION / browser
            remove_dir = os.path.normpath(join(old_ua.path, os.pardir))
            log.info("Deleting old browser directory: %s" % remove_dir)
            if os.path.isdir(remove_dir):
                RemoveDirectory(remove_dir)
        old_ua.enabled = False
        if not self.dummy_db_mode:
            old_ua.save()

    def _register_new_ua(self, browser_dir, version, generation):
        ua = Useragent()
        ua.id = "%s-%s-%s" % (self.branch_id, version, self.platform)
        ua.enabled = True
        ua.platform = self.platform
        ua.branch_id = self.branch_id
        ua.version = version
        ua.generation = generation
        # This is a special value meaning that it should be updated with the
        # real useragent string the first time a request is received by the
        # testrunner.
        ua.name = "pending_update"
        ua.path = self._get_path(browser_dir, version)
        log.info("Registering new ua: %s", ua)
        if not self.dummy_db_mode:
            ua.save()

    def update(self, dummy_db_mode=False):
        log.info("=" * 80)
        log.info("Updating %s", self)
        try:
            Branch.objects.get(pk=self.branch_id)
        except Branch.DoesNotExist:
            raise Exception("Branch '%s' referenced by '%s' does not exist" % (
                            self.branch_id, self))

        self.dummy_db_mode = dummy_db_mode

        MaybeMakeDirectory(self.base_dir)
        if self._is_fresh():
            return

        version = self._get_version()
        # Insert the version in the url if dynamic
        self.url = self.url % {"version": version}
        browser_dir = join(self.base_dir, version)

        uas = Useragent.objects.filter(branch=self.branch_id, enabled=True,
                                       platform=self.platform)
        log.debug("uas %s", uas)
        assert len(uas) <= 1, ("More than one active Useragent for this branch "
                               "and platform")
        prev_generation = 0
        if len(uas) == 1:
            old_ua = uas[0]
            if old_ua.version == version:
                log.info("Browser already installed, nothing to do")
                return
            prev_generation = old_ua.generation
            self._uninstall_ua(browser_dir, old_ua)

        log.debug("browser_dir: %s", browser_dir)
        if os.path.exists(browser_dir):
            log.warn("New browser directory already exists (%s), will be "
                     "deleted" % browser_dir)
        RemoveDirectory(browser_dir)
        MaybeMakeDirectory(browser_dir)

        self._download_and_install(self.url, browser_dir, version)
        self._register_new_ua(browser_dir, version, prev_generation + 1)


class Mozilla(BrowserUpdater):
    def __init__(self, base_dir, branch_id, platform, url, version):
        super(Mozilla, self).__init__(
            base_dir=base_dir,
            branch_id=branch_id,
            platform=platform,
            url=url,
        )
        self.version = version

        platform_to_package_dir = {
            "win": "firefox", # XXX not correct if installed from .exe
            "lin": "firefox",
            "mac": "Firefox.app", # XXX not correct for non official builds.
        }
        self.package_dir = platform_to_package_dir[self.platform]

    def _get_path(self, browser_dir, version):
        return join(browser_dir, self.package_dir)

    def _get_version(self):
        return self.version

class TimestampFreshnessCheckerMixin(object):
    def _is_fresh(self):
        log.debug("Checking freshness")
        stamp_file = join(self.base_dir, "last_check_stamp.txt")
        if not os.path.isfile(stamp_file):
            mtime = 0
        else:
            mtime = os.stat(stamp_file).st_mtime
        age = time.time() - mtime
        is_fresh = age < self.check_interval
        log.info("Stamp age: %s (check_interval: %s fresh: %s)", age,
                 self.check_interval, is_fresh)
        if not is_fresh:
            with open(stamp_file, "w") as f:
                f.write(str(time.time()))
        return is_fresh


class MozillaNightly(TimestampFreshnessCheckerMixin, BrowserUpdater):
    def __init__(self, base_dir, branch_id, platform, url, check_interval):
        super(MozillaNightly, self).__init__(
            base_dir=base_dir,
            branch_id=branch_id,
            platform=platform,
            url=url,
        )
        self.check_interval = check_interval

        # XXX duplicate, maybe ok of the names are different
        platform_to_package_dir = {
            "win": "firefox", # XXX not correct if installed from .exe
            "lin": "firefox",
            "mac": "Firefox.app", # XXX not correct for non official builds.
        }
        self.package_dir = platform_to_package_dir[self.platform]

        self.browser_temp_dir = None

    def _download_and_install(self, url, browser_dir, version):
        assert self.browser_temp_dir, "_get_version() was not called"
        # The browser was already fetched in _get_version() (to get the version).
        # just need to rename the path here.
        os.rmdir(browser_dir)
        os.rename(self.browser_temp_dir, browser_dir)

    def _get_path(self, browser_dir, version):
        return join(browser_dir, self.package_dir)

    def _get_version(self):
        # BuildID (used for the version) is inside the archive, so it is
        # fetched here (thus doing part of the job of _download_and_install()
        # in advance).
        self.browser_temp_dir = join(self.base_dir, "temp")
        RemoveDirectory(self.browser_temp_dir)
        MaybeMakeDirectory(self.browser_temp_dir)

        dest = self._download(self.url, self.browser_temp_dir)
        self._extract_archive(dest, self.browser_temp_dir)

        if self.platform == "mac":
            inipath = join(self.browser_temp_dir, self.package_dir,
                           "Contents", "MacOS", "application.ini")
        else:
            inipath = join(self.browser_temp_dir, self.package_dir,
                           "application.ini")
        assert os.path.isfile(inipath), "Can't find application.ini (%s)" % inipath
        build_id = None
        source_stamp = None
        with open(inipath) as f:
            for line in f:
                line = line.strip()
                if line.startswith('BuildID'):
                    build_id = line.split("=")[1]
                if line.startswith('SourceStamp'):
                    source_stamp = line.split("=")[1]
        assert build_id and source_stamp, ("Couldn't find build_id and "
                                           "source_stamp in application.ini")
        log.debug("build_id: %s, source_stamp: %s", build_id, source_stamp)

        # XXX save source_stamp for later use

        return build_id

class ChromeNightly(TimestampFreshnessCheckerMixin, BrowserUpdater):
    def __init__(self, base_dir, branch_id, platform, url, version_url, check_interval):
        super(ChromeNightly, self).__init__(
            base_dir=base_dir,
            branch_id=branch_id,
            platform=platform,
            url=url,
        )
        self.version_url = version_url
        self.check_interval = check_interval

    def _get_path(self, browser_dir, version):
        # Will need change once other platforms are available.
        return join(browser_dir, "chrome-win32")

    def _get_version(self):
        log.debug("Fetching version from %s" % self.version_url)

        f = urllib2.urlopen(self.version_url)
        version = f.read().strip()
        f.close()
        log.debug("Got version '%s'", version)
        return version

class WebKitNightlyWin(TimestampFreshnessCheckerMixin, BrowserUpdater):
    def __init__(self, base_dir, branch_id, url, version_url, check_interval):
        super(WebKitNightlyWin, self).__init__(
            base_dir=base_dir,
            branch_id=branch_id,
            platform="win",
            url=url,
        )
        self.version_url = version_url
        self.check_interval = check_interval

    def _get_safari_dir(self):
        # See http://trac.webkit.org/browser/trunk/WebKitTools/FindSafari/FindSafari.cpp#L80

        # from https://comtypes.svn.sourceforge.net/svnroot/comtypes/trunk/comtypes/GUID.py
        import ctypes
        import _winreg

        _CLSIDFromString  = ctypes.oledll.ole32.CLSIDFromString
        _StringFromCLSID = ctypes.oledll.ole32.StringFromCLSID
        _CoTaskMemFree = ctypes.oledll.ole32.CoTaskMemFree
        BYTE = ctypes.c_byte
        WORD = ctypes.c_ushort
        DWORD = ctypes.c_ulong

        class GUID(ctypes.Structure):
            _fields_ = [("Data1", DWORD),
                        ("Data2", WORD),
                        ("Data3", WORD),
                        ("Data4", BYTE * 8)]

            def __init__(self, name=None):
                if name is not None:
                    _CLSIDFromString(unicode(name), ctypes.byref(self))

            def __repr__(self):
                return u'GUID("%s")' % unicode(self)

            def __unicode__(self):
                p = ctypes.c_wchar_p()
                _StringFromCLSID(ctypes.byref(self), ctypes.byref(p))
                result = p.value
                _CoTaskMemFree(p)
                return result
            __str__ = __unicode__

        clsid = str(GUID("WebKit.WebView"))
        log.debug("Found clsid: %s", clsid)

        key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,
                              r"SOFTWARE\Classes\CLSID\%s\InprocServer32" % clsid)
        webkit_dll = _winreg.QueryValueEx(key, None)[0]
        key.Close()
        log.debug("reg value. %s", webkit_dll)
        assert "WebKit.dll" in webkit_dll
        safari_dir = webkit_dll.replace("WebKit.dll", "")
        log.debug("Safari dir: %s", safari_dir)
        return safari_dir

    def _get_version(self):
        f = urllib2.urlopen(self.version_url)
        dom = xml.dom.minidom.parseString(f.read())
        f.close()
        first_item = dom.getElementsByTagName("item")[0]
        version = first_item.getElementsByTagName("title")[0].firstChild.data[1:]
        log.debug("Got version '%s'", version)
        return version

    def _download_and_install(self, url, browser_dir, version):
        super(WebKitNightlyWin, self)._download_and_install(url, browser_dir,
                                                            version)

        # This replicates the steps executed in the batch script generated by
        # FindSafari.exe /printSafariLauncher
        # See http://trac.webkit.org/browser/trunk/WebKitTools/FindSafari/FindSafari.cpp#L230

        safari_dir = self._get_safari_dir()

        target_dir = self._get_path(browser_dir, version)
        os.mkdir(target_dir)
        log.debug("target_dir: %s", target_dir)
        shutil.copytree(join(safari_dir, "Safari.resources"), join(target_dir, "Safari.resources"))
        shutil.copytree(join(safari_dir, "plugins"), join(target_dir, "plugins"))

        webkit_dir = join(browser_dir, "WebKit-r%s" % version)
        shutil.copy(join(webkit_dir, "WebKit.dll"), target_dir)
        shutil.copy(join(webkit_dir, "WebKit.pdb"), target_dir)
        shutil.copytree(join(webkit_dir, "WebKit.resources"), join(target_dir, "WebKit.resources"))

        # Safari.exe has to be updated with the FindSafari.exe command.
        # That command update Safari.exe from an hardcoded path in
        # $TMP/WebKitNightly/Safari.exe (it is normally used from the batch
        # script which copies everything in $TMP).
        # So we copy Safari.exe there, run the command to update the manifest
        # and finally copy Safari.exe to its destination. Kind of hacky.
        log.debug("Updating Safari.exe manifest")
        safari_tmp = join(os.environ["TMP"], "WebKitNightly")
        RemoveDirectory(safari_tmp)
        os.mkdir(safari_tmp)
        shutil.copy(join(safari_dir, "Safari.exe"), safari_tmp)
        subprocess.check_call([join(webkit_dir, "FindSafari.exe"),
                               "/updateManifest"])
        shutil.copy(join(safari_tmp, "Safari.exe"), target_dir)
        RemoveDirectory(safari_tmp)

        # Utility script to run webkit (not used by browser.py currently).
        run_webkit_cmd = """
@echo off
cd %(target_dir)s
set PATH=%(webkit_dir)s;%(safari_dir)s;%%PATH%%
safari "%%*""
        """
        with open(join(target_dir, "run-webkit.cmd"), "wb") as f:
            f.write(run_webkit_cmd % {
                "target_dir": target_dir,
                "webkit_dir": webkit_dir,
                "safari_dir": safari_dir,
            })
        # browser.py will add these additional paths to the PATH environment
        # variable before running Safari.exe
        with open(join(target_dir, "additional_paths.txt"), "wb") as f:
            f.write(";".join([webkit_dir, safari_dir]))

    def _get_path(self, browser_dir, version):
        return join(browser_dir, "WebKitNightly")

def browsers_updater(browsers_config_path, update_status=False,
                     dummy_db_mode=False):
    loc = {}
    execfile(browsers_config_path, {}, loc)
    assert "browsers" in loc, ("Configuration '%s' did not set a 'browsers' "
                               "list" % browsers_config_path)

    # XXX status update code duplicated with importer. maybe share in utils.
    if update_status:
        update_start_time = time.time()
        status = Status.objects.get(pk=1)
        if status.generation == Status.BUSY:
            log.warn("Status in busy state, another import in progress?")
            sys.exit(3)
            return
        last_generation = status.generation
        log.debug("last_generation: %s", last_generation)
        status.generation = Status.BUSY
        status.save()
        log.info("Waiting a bit for tests to finish...")
        time.sleep(20)
        log.info("...done")
    try:
        for browser in loc["browsers"]:
            browser.update(dummy_db_mode)
    finally:
        if update_status:
            status.generation = last_generation + 1
            status.save()
            log.debug("new generation: %s", status.generation)
            log.info("Update duration: %s", time.time() - update_start_time)

def main():
    usage = "%prog [OPTIONS] BROWSERS_CONFIG_FILE"
    parser = OptionParser(usage=usage)
    parser.add_option("--dummy-db", action="store_true", default=False,
        help="Do not write to the database (but still fetch and install new "
             "browsers)")
    parser.add_option("--update-status", action="store_true", default=False,
        help="Update the status table to stop pending tests during browser "
             "update")

    parser = cli_wrapper.add_common_parser_options(parser)
    options, args = parser.parse_args()

    if len(args) != 1:
        parser.print_help()
        sys.exit(2)

    browsers_config_path = args[0]
    cli_wrapper.process_options(options)

    browsers_updater(browsers_config_path, options.update_status,
                     options.dummy_db)

if __name__ == "__main__":
    main()
